package com.cgrs572.shortlink.admin.common.biz.user;

import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Lists;
import com.cgrs572.shortlink.admin.common.convention.exception.ClientException;
import com.cgrs572.shortlink.admin.common.convention.result.Results;
import com.cgrs572.shortlink.admin.config.UserFlowRiskControlConfiguration;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Optional;
import static com.cgrs572.shortlink.admin.common.convention.errorcode.BaseErrorCode.FLOW_LIMIT_ERROR;

/**
 * 用户操作流量风控过滤器
 */
@Slf4j
@RequiredArgsConstructor
public class UserFlowRiskControlFilter implements Filter {
    // 构造器注入
    private final StringRedisTemplate stringRedisTemplate;
    private final UserFlowRiskControlConfiguration userFlowRiskControlConfiguration;

    private static final String USER_FLOW_RISK_CONTROL_LUA_SCRIPT_PATH = "lua/user_flow_risk_control.lua";

    @SneakyThrows
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        // 获取并加载lua脚本
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(USER_FLOW_RISK_CONTROL_LUA_SCRIPT_PATH)));
        redisScript.setResultType(Long.class); // 设置lua脚本的返回值currentAccessCount的类型---注意：lua脚本返回的数值默认就是Long
        // 获取用户名---由于所有请求来了以后都会先经过用户信息传输过滤器(UserTransmitFilter),而该过滤器后将用户信息存储到应用上下文中，但是会对“用户登录”以及“检查用户名是否可用”的这两个请求放行
            // 所以若遇到用户信息传输过滤器(UserTransmitFilter)放行的请求就无法通过应用上下文来获取用户名，此时就是默认为"other"
        String username = Optional.ofNullable(UserContext.getUsername()).orElse("other");
        Long result = null; // 记录用户访问频率
        try {
            // Redis执行lua脚本，实现原子性操作（原子性：事务是不可分割的最小操作单元，要么全部成功，要么全部失败）
                // execute方法用于执行 Redis 命令或 Lua 脚本，此处用来执行lua脚本
            // 参数一：要执行的lua脚本
            // 参数二：传递给lua脚本的keys参数列表哦
            // 参数三：传递给lua脚本的ARGV参数---此处参数值为流量风控时间窗(单位：秒)
            // result：返回的lua脚本的执行结果
            /*
             * 为什么使用execute()方法执行lua脚本？
             * 1.lua脚本在redis中单线程执行能够保持原子性
             * 2.多个操作可以在一个脚本中调用完成，减少网络开销
             * 3.可以实现Redis命令无法直接完成的复杂业务逻辑
             */
            result = stringRedisTemplate.execute(redisScript, Lists.newArrayList(username), userFlowRiskControlConfiguration.getTimeWindow());
        } catch (Throwable ex) {
            log.error("执行用户请求流量限制LUA脚本出错", ex);
            returnJson((HttpServletResponse) response, JSON.toJSONString(Results.failure(new ClientException(FLOW_LIMIT_ERROR))));
        }
        // 若用户访问频率超出流量风控时间窗口内可访问次数,则抛出一个客户端异常,表明"当前系统繁忙，请稍后再试",以此来避免攻击
        if (result == null || result > userFlowRiskControlConfiguration.getMaxAccessCount()) {
            returnJson((HttpServletResponse) response, JSON.toJSONString(Results.failure(new ClientException(FLOW_LIMIT_ERROR))));
        }
        filterChain.doFilter(request, response); // 放行
    }

    private void returnJson(HttpServletResponse response, String json) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try (PrintWriter writer = response.getWriter()) {
            writer.print(json);
        }
    }
}
